# Local Images Directory Editor
#
# Copyright (C) 2010 Yotam Medini <yotam.medini@gmail.com>
#
# This file is part of Gupicasa.
#
# Gupicasa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Gupicasa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gupicasa.  If not, see <http://www.gnu.org/licenses/>.


import dircache
import gobject
import gtk
import os
import pyexiv2
import re
import string
import threading


try:
    pyexiv2_version = pyexiv2.version_info
except:
    pyexiv2_version = (0, 0, 0)
if pyexiv2_version < (0, 2, 0):
    import sys
    sys.stderr.write("Gupicasa requires pyexiv2 of versio (0,2,0) or later\n")
    sys.exit(1)

# local
import arith
import cal
import convert
import debug
import lru
import mosaic
import raw
import rect
import ugtk
import uicommon
import usys


exif_orientation_to_rotation_quad = {
    1: (0, "Top-Left", "TL"),
    8: (1, "Left-Bottom", "LB"),
    3: (2, "Bottom-Right", "BR"),
    6: (3, "Right-Top", "RT"),
}

rot_quad_to_gdk_rot = [
    gtk.gdk.PIXBUF_ROTATE_NONE,
    gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE,
    gtk.gdk.PIXBUF_ROTATE_UPSIDEDOWN,
    gtk.gdk.PIXBUF_ROTATE_CLOCKWISE]


class EdImage:


    class Value:

        def __init__(self, wh, rq, dt):

            # wh: relative to original pixbuf, independent of orientation
            # rot_quad: Original: exif-orientation or 0,
            #           Edited: relative to original

            self.wh = wh
            self.rot_quad = rq
            self.datetime = dt


        def clone(self):
            debug.log("wh=%s, rq=%d, dt=%s" %
                      (self.wh, self.rot_quad, self.datetime))
            return self.__class__(self.wh, self.rot_quad, self.datetime)


    def __init__(self, ide, i, filename, pixbuf):
        self.ide = ide
        self.i = i
        self.filename = filename
        self.saved_filename = None
        self.caption = None
        [self.orientation, dt] = self.init_metadata_orientaion_time()
        orientation_quad = exif_orientation_to_rotation_quad.get(
            self.orientation, (-1,""))[0]
        self.init_pixbuf_small(pixbuf, orientation_quad)
        wh_original = (pixbuf.get_width(), pixbuf.get_height())
        # if orientation_quad in (1,3):
        #     wh_original = (wh_original[1], wh_original[0])
        self.value_original = EdImage.Value(wh_original, orientation_quad, dt)
        self.value_edited = self.value_original.clone()
        self.value_saved = None
        self.saved = False # If some edited changes are saved
        self.dirty = False # Changes made relative to saved version


    def init_metadata_orientaion_time(self):
        "Get EXIF-metadata of image orientation and original time"
        ot = [None, None]
        try:
            metadata = pyexiv2.ImageMetadata(self.filename)
            metadata.read()
        except Exception, e:
            debug.log("EdImage: %s" % str(e))
            metadata = None
        ot = (None, None)
        if metadata is not None:
            ot = map(lambda key: self.metadata_value(metadata, key),
                     ["Image.Orientation", "Photo.DateTimeOriginal"])
            debug.log("ot=%s" % str(ot))
            # del metadata
        return ot


    def metadata_value(self, metadata, key):
        v = None
        try:
            tag = metadata["Exif." + key]
            v = tag.value
        except:
            tag = None
        return v


    def init_pixbuf_small(self, pixbuf, orientation_quad):
        wh_original = (pixbuf.get_width(), pixbuf.get_height())
        debug.log("pixbuf:wh=(%d,%d)" % wh_original)
        if orientation_quad in (1, 3):
            # We need to map as will be finally rotated
            wh_original = (wh_original[1], wh_original[0])
            wh = self.ide.mapper.wh_map(wh_original)
            wh = (wh[1], wh[0])
        else:
            wh = self.ide.mapper.wh_map(wh_original)
        self.pixbuf_small = pixbuf.scale_simple(wh[0], wh[1],
                                                gtk.gdk.INTERP_TILES)
        debug.log("wh_original=%s, wh=%s, orquad=%d" %
                  (str(wh_original), str(wh), orientation_quad))
        if orientation_quad > 0:
            gdk_rot = rot_quad_to_gdk_rot[orientation_quad]
            debug.log("gdk_rot=%d" % gdk_rot)
            pb_rot = self.pixbuf_small.rotate_simple(gdk_rot)
            del self.pixbuf_small
            self.pixbuf_small = pb_rot


    def pixbuf_detailed(self):
        debug.log("")
        pb = self.ide.get_pixbuf(self)
        debug.log("pb=%s" % pb)
        return pb


    def get_datetime(self):
        return self.value_edited.datetime


    def set_datetime(self, dt):
        debug.log("i=%d" % self.i)
        self.value_edited.datetime = dt
        self.dirty = True


    def wh_edited_scale(self, d):
        wh = self.value_original.wh
        wh = (wh[0]/d, wh[1]/d)
        self.set_wh(wh)


    def set_wh(self, wh):
        self.value_edited.wh = wh
        debug.log("i=%d, wh_edited=%s" % (self.i, str(self.value_edited.wh)))
        self.dirty = True


    def wh_edited_check_set(self, w, h):
        debug.log("i=%d" % self.i)
        if self.value_original.rot_quad % 2 != 0:
            t = w;  w = h;  h = t;
        (worig, horig) = self.value_original.wh
        ok = (w <= worig) and (h <= horig) and (
            ( (worig == horig) == (w == h) ) and
            ( (worig < horig) == (w < h) ) )
        if ok:
            self.value_edited.wh = (w, h)
            self.dirty = True
        return ok


    def get_wh(self, original):
        return self.value_original.wh if original else self.value_edited.wh


    def get_rotation(self, original):
        value_set = self.value_original if original else self.value_edited
        return value_set.rot_quad


    def set_rotation_relative(self, n):
        debug.log("i=%d" % self.i)
        self.value_edited.rot_quad = (self.value_original.rot_quad + n) % 4
        debug.log("rq: +n=%d, original=%d, edited=%d" %
              (n, self.value_edited.rot_quad, self.value_original.rot_quad))
        self.dirty = True



    def get_rotation_relative(self, rot_quad=None):
        if rot_quad is None:
            rot_quad = self.value_edited.rot_quad
        n = (rot_quad - self.value_original.rot_quad + 4) % 4
        return n


    def wh_strget(self, original):
        wh = self.get_wh(original)
        if self.value_original.rot_quad % 2 != 0: # rotation of 90 or 270
            wh = (wh[1], wh[0])
        return "%dx%d" % wh


    def wh_original_strget(self):
        return self.wh_strget(original=True)

    def wh_edited_strget(self):
        return self.wh_strget(original=False)


    def orientation_text(self):
        s = "Exif.Image.Orientation: "
        n = self.orientation
        if n <= 0:
            s += "Undefined"
        else:
            qt = exif_orientation_to_rotation_quad[n]
            s += ("\n(%d) %s" % (n, qt[1]))
        return s


    def save(self, dn):
        debug.log("")
        bn = os.path.basename(self.filename)
        self.saved_filename = "%s/%s" % (dn, bn)
        metadata = {}
        metadata["Exif.Photo.DateTimeDigitized"] = cal.datetime_now()
        dt = self.value_edited.datetime
        if dt is not None:
            metadata['Exif.Photo.DateTimeOriginal'] = dt
        if (self.value_edited.wh != self.value_original.wh or
            self.value_edited.rot_quad != self.value_original.rot_quad):
            metadata["Exif.Photo.PixelXDimension"] = self.value_edited.wh[0]
            metadata["Exif.Photo.PixelYDimension"] = self.value_edited.wh[1]
        if self.value_edited.rot_quad != self.value_edited.rot_quad:
            pass  # eliminiate
        err = convert.convert(self.filename, self.saved_filename,
                              self.value_edited.wh,
                              self.get_rotation_relative(), metadata)
        if err is None:
            self.value_saved = self.value_edited.clone()
            self.saved = True
            self.dirty = False
        else:
            self.saved_filename = None
            self.ide.common.warning(err)
        return self.saved_filename


    def revert_to_original(self):
        debug.log("")
        self.value_edited = self.value_original.clone()
        self.value_saved = None
        if self.saved_filename is not None:
            usys.unlink(self.saved_filename)
            self.saved_filename = None
        self.saved = False
        self.dirty = False


    def revert_to_saved(self):
        self.value_edited = self.value_saved.clone()
        self.dirty = False


class DirReader:


    def __init__(self, ide):
        self.ide = ide
        self.listdir = dircache.listdir(ide.dirname)
        self.ei = 0


    def step(self):
        status_text = None
        n = len(self.listdir)
        if self.ei < n:
            bfn = self.listdir[self.ei]
            fn = "%s/%s" % (self.ide.dirname, bfn)
            try:
                pixbuf = gtk.gdk.pixbuf_new_from_file(fn)
            except:
                pixbuf = None
                debug.log("%s: Ignored" % fn)
            if pixbuf:
                debug.log("%s: Adding" % fn)
                self.ide.images.append(
                    EdImage(self.ide, len(self.ide.images), fn, pixbuf) )
                del pixbuf
            status_text = ("Loaded %d images. Read %d of %d entries" %
                           (len(self.ide.images), self.ei, n))
            self.ei += 1
        return status_text


class UploadThread(threading.Thread):


    def __init__(self, uploader):
        threading.Thread.__init__(self)
        self.uploader = uploader


    def run(self):
        debug.log("UploadThread")
        u = self.uploader
        ed_img = u.ide.images[u.indices[u.ei]]
        u.upload_one(ed_img, u.n_uploaded_target)
        debug.log("Add idle event for set_done")
        # Add idle event
        self.uploader.thread_done = True
        gobject.idle_add(self.set_done) # So main thread will notice


    def set_done(self):
        debug.log("")


class Uploader:


    def __init__(self, ide, upload_one, n_uploaded_target):
        self.ide = ide
        self.upload_one = upload_one
        self.n_uploaded_target = n_uploaded_target
        self.ei = 0
        self.indices = list(self.ide.tbl_mosaic.selected_indices)


    def step(self):
        status_text = None
        n = len(self.indices)
        if self.ei < n:
            ed_img = self.ide.images[self.indices[self.ei]]
            # self.upload_one(ed_img, self.n_uploaded_target)
            self.thread_done = False
            up_thread = UploadThread(self)
            up_thread.start()
            while not self.thread_done:
                debug.log("Blocking for events")
                gtk.main_iteration()
                debug.log("Got and done event")
                ugtk.gtk_update()
                
            # Now block on gtk events
            debug.log("{ join...")
            up_thread.join()
            debug.log("} joined")
            self.ei += 1
            status_text = "Uploaded %d photos out of %d" % (self.ei, n)
        return status_text


class ImgDirEditor:


    pattern_wwwxhhh = re.compile("^([1-9][0-9]*)x([1-9][0-9]*)$")


    def __init__(self, ui, cmdbox, common=None, dirname=None):
        debug.log("")
        self.ui = ui
        self.cmdbox = cmdbox
        stand_alone = (common is None)
        if stand_alone:
            common = uicommon.Common()
            common.warning("Stand Alone version")
        self.common = common
        self.last_scr_clear = None
        common.preferences.observers.append(self.prefs_observer)
        self.detailed_mode_original = True
        self.scale2fit = True
        mm = self.common.preferences.thumb_size_mm
        self.mapper = rect.Mapper(mm[0], mm[1])
        self.images = []
        self.current_image = -1
        self.pixbufs_cache = lru.Cache()
        self.pixels_divisors = gtk.ListStore(str, int)
        self.current_wh = None
        self.mainwin = self.create_gui(stand_alone)
        self.opendir(dirname)
        self.set_edit_mode()


    def __del__(self):
        debug.log("")
        self.clear_scratch_dir()


    def opendir(self, dirname):
        debug.log("dirname=%s" % dirname)
        self.clear_scratch_dir()
        scrdir = self.common.preferences.scratch_dir
        if not usys.dexist(scrdir):
            debug.log("Opening %s" % scrdir)
            os.mkdir(scrdir)
        for i in self.images:
            del i
        self.images = []
        self.dirname = dirname
        if dirname is not None:
            done = self.readdir()
            if done:
                n = len(self.images)
                gis = self.tbl_mosaic.place_images(n, self.pick_detailed)
                for i in range(n):
                    gi = gis[i]
                    pixbuf = self.images[i].pixbuf_small
                    gi.set_from_pixbuf(pixbuf)
                self.tbl_mosaic.show_all()
                if n == 0:
                    self.empty_detailed()
            self.status_text_set("Loading local dir %s" %
                                 ("done" if done else "canceled"))


    def readdir(self):
        reader = DirReader(self)
        monitor = ugtk.Monitor(title="Loading images from local directory",
                               parent=self.common.topwin,
                               step_cb=reader.step,
                               status_widgets=[self.common.statusbar])
        monitor.set_icon(self.common.icons_pixbuf["gupicasa"])
        done = monitor.run_monitor()
        monitor.destroy()
        return done


    def create_gui(self, stand_alone):
        self.edit_action_widgets = []
        pane = gtk.VPaned()
        w = pane
        if stand_alone:
            w = gtk.VBox()
            w.pack_start(pane, expand=True, fill=True, padding=1)
            w.pack_start(self.common.statusbar, expand=False)
        b = self.create_raw_format_conversion_button()
        self.cmdbox.pack_start(b, expand=False, fill=True, padding=2)
        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        nzc = self.common.preferences.mosaic_zcolumns()
        self.label_n_selected = gtk.Label("")
        self.tbl_mosaic = mosaic.Mosaic(nzc, self.label_n_selected)
        sw.add_with_viewport(self.tbl_mosaic)
        pane.pack1(sw, resize=True, shrink=True)
        pane.pack2(self.create_detailed_window(), resize=True, shrink=True)
        self.cmdbox.pack_start(self.label_n_selected, expand=False, fill=True,
                               padding=2)
        self.create_menu()
        return w


    def create_raw_format_conversion_button(self):
        b = ugtk.StockButton(gtk.STOCK_CONVERT)
        b.connect('clicked', self.raw_convert)
        b.set_tooltip_text("""
Convert raw formatted images files
into JPEG formatted files.
Example of raw formats:
  .crw .cr2 (Canon)
  .nef .nrw (Nikon)
  .orf (Olympus)"""[1:])
        return b


    def create_detailed_window(self):
        vb = gtk.VBox()

        hb = gtk.HBox()
        radio_group = []
        hb.pack_start(self.create_info_frame(radio_group), padding=6)
        hb.pack_start(self.create_edit_frame(radio_group), padding=6)
        hb.pack_start(self.create_persist_commands(),
                      expand=False, fill=False, padding=6)
        vb.pack_start(hb, expand=False, fill=False)

        caption_box = gtk.HBox()
        caption_box.pack_start(gtk.Label("Caption:"), expand=False, fill=False,
                                         padding=2)
        self.caption_entry = e = gtk.Entry()
        e.connect('activate', self.update_caption)
        e.connect('focus-out-event', self.update_caption)
        caption_box.pack_start(e, expand=True, fill=True, padding=2)
        vb.pack_start(caption_box, False, False, padding=1)
        ugtk.add_fit_native_zoom_control(self.cmdbox, self.set_zoom_mode)
        hb = gtk.HBox()
        # hb.pack_start( self.cmd_buttons(), False, False, 1)
        self.detailed_image = gtk.Image()
        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        sw.add_with_viewport(self.detailed_image)
        hb.pack_start(sw, True, True, 2)
        vb.pack_start(hb, True, True, 2)

        return vb


    def create_info_frame(self, radio_group):
        f = gtk.Frame()
        rb = gtk.RadioButton(None, "Original")
        radio_group.append(rb)
        f.set_label_widget(rb)
        tbl = gtk.Table(2, 3)
        self.filename = gtk.Entry()
        self.pixels = gtk.Entry()
        self.time = gtk.Entry()
        r = 0
        for lbl, e, nc in (("Filename:", self.filename, -1),
                           ("Pixels:", self.pixels, 10),
                           ("Time:", self.time, 26)):
            e.set_editable(False)
            ugtk.attach1(tbl, gtk.Label(lbl), 0, r, xoptions=0)
            pos = None
            if nc > 0:
                pos = gtk.POS_LEFT
                e.set_max_length(nc)
                e.set_width_chars(nc)
            ugtk.attach1(tbl, e, 1, r, pos=pos)
            r += 1
        tbl.set_border_width(2)
        f.add(tbl)
        return f


    def create_edit_frame(self, radio_group):
        f = gtk.Frame()
        rb = gtk.RadioButton(radio_group[0], "Edited")
        rb.connect('clicked', self.set_edit)
        f.set_label_widget(rb)
        tbl = gtk.Table(2, 3)
        self.efilename = gtk.Entry()
        self.efilename.set_editable(False)
        self.epixels = epixels = gtk.ComboBoxEntry(self.pixels_divisors, 0)
        self.epixels_handler =  epixels.connect('changed', self.scale)
        epixels.child.connect('activate', self.arbitrary_scale)
        epixels.child.connect('focus-out-event', self.arbitrary_scale)
        rotbox = self.create_rotation_box()
        hb_pixels_rotation = gtk.HBox()
        hb_pixels_rotation.pack_start(epixels)
        hb_pixels_rotation.pack_end(rotbox)
        self.etime = gtk.Entry()
        self.etime.set_editable(False)
        b = ugtk.StockButton(gtk.STOCK_EDIT)
        b.connect('clicked', self.select_date_time, None)
        b.set_tooltip_text("Select Date/Time")
        time_edit = gtk.HBox()
        time_edit.pack_start(self.etime)
        time_edit.pack_end(b, expand=False, fill=False, padding=1)
        r = 0
        for lbl, e in (("Filename:", self.efilename),
                       ("Pixels:", hb_pixels_rotation),
                       ("Time:", time_edit)):
            pos = None if r == 0 else gtk.POS_LEFT
            ugtk.attach1(tbl, gtk.Label(lbl), 0, r, xoptions=0, pos=pos)
            ugtk.attach1(tbl, e, 1, r, pos=pos)
            r += 1
        tbl.set_border_width(2)
        f.add(tbl)
        self.edit_action_widgets += [epixels, b]
        return f


    def create_rotation_box(self):
        hbox = gtk.HBox()
        hbox.pack_start(gtk.Label("Rotate:"), expand=True, fill=True, padding=3)
        cb = gtk.combo_box_new_text()
        degree_sign = unichr(0x00B0)  # degree UTF-8: 0xC2 0xB0  UTF-16: 0x00B0
        for a in range(4):
            cb.append_text(("%d" % (90*a)) + degree_sign)
        cb.set_active(0)
        self.rotate_handler = cb.connect('changed', self.rotate)
        hbox.pack_start(cb, expand=False, fill=False, padding=3)
        self.combox_rotation = cb
        self.edit_action_widgets.append(cb)
        return hbox


    def create_persist_commands(self):
        box = gtk.VBox()
        bsave = ugtk.StockButton(gtk.STOCK_SAVE)
        box.pack_start(bsave)
        brevert = ugtk.StockButton(gtk.STOCK_REVERT_TO_SAVED)
        box.pack_start(brevert)
        bremove = ugtk.StockButton(gtk.STOCK_REMOVE)
        box.pack_start(bremove)

        bsave.set_tooltip_text("Save scaling, rotation\nand time to file")
        brevert.set_tooltip_text("Revert scaling, rotation\nand time as saved")
        bremove.set_tooltip_text("Ignore edited changes,\nuse original")

        bsave.connect('clicked', self.save)
        brevert.connect('clicked', self.revert_to_saved)
        bremove.connect('clicked', self.revert_to_original)

        self.button_save = bsave
        self.button_revert = brevert
        self.button_remove = bremove
        return box


    def create_menu(self):
        debug.log("cmdbox=%s" % str(self.cmdbox))
        entries = (
            ("EditMenu", None, "_Edit"),
            ("LocalMenu", gtk.STOCK_HARDDISK, "_Local"),
            ("SelectMenu", None, "_Select"),
            ("SelectedMenu", None, "Selected"),
            ("SelectAll", gtk.STOCK_SELECT_ALL,
             "Select _All",  None,
             "Select all photos/images in local",
             self.select_all),
            ("UnselectAll", None,
             "_Unselect All",  None,
             "Unselect all photos/images in local",
             self.unselect_all),
            ("SelectUnsaved", None,
             "Select unsaved",  None,
             "Select edited photos/images in local\n" +
             "that were not yet saved",
             self.select_edited_unsaved),
            ("SelectTimeless", None,
             "Select time-date-less",  None,
             "Select photos/images without time/date stamp",
             self.select_timeless),
            ("SelectSameDims", None,
             "Select same dimensions",  None,
             "Select photos/images of same dimension\n" +
             "as current one",
             self.select_same_dimensions),
            ("SameScale", None,
             "Scale selected",  None,
             "Scale selected photos/images\n" +
             "with same dimensions\n" +
             "as current one is scaled",
             self.selected_same_scale),
            ("SetSameTime", None,
             "Set same time on selected",  None,
             "Set same time/date stamp on selected photos/images\n" +
             "as on current one",
             self.selected_set_same_time),
            ("SameShiftTime", None,
             "Shift time/date on selected",  None,
             "Shift same time/date stamp on selected photos/images\n" +
             "as shifted on current one",
             self.selected_shift_same_time),
            ("SelectedSave", None,
             "Save selected",  None,
             "Save selected edited photos/images",
             self.selected_save),
            ("SelectedRemove", None,
             "Ignore saved selected",  None,
             "Remove edited versions of\n" +
             "selected edited photos/images.\n" +
             "Originals are left intact.",
             self.selected_remove),
            )
        ui_info = \
'''<ui>
  <menubar name='MenuBar'>
    <menu action='EditMenu'>
      <menu action='LocalMenu'>
        <menu action='SelectMenu'>
          <menuitem action='SelectAll'/>
          <menuitem action='UnselectAll'/>
          <menuitem action='SelectUnsaved'/>
          <menuitem action='SelectTimeless'/>
          <menuitem action='SelectSameDims'/>
        </menu>
        <menu action='SelectedMenu'>
          <menuitem action='SameScale'/>
          <menuitem action='SetSameTime'/>
          <menuitem action='SameShiftTime'/>
          <menuitem action='SelectedSave'/>
          <menuitem action='SelectedRemove'/>
        </menu>
      </menu>
    </menu>
  </menubar>
</ui>'''

        actions = gtk.ActionGroup("LocalActions")
        actions.add_actions(entries)
        self.ui.insert_action_group(actions, 0)
        mergeid = self.ui.add_ui_from_string(ui_info)
        mb = self.ui.get_widget("/MenuBar")
        self.local_menu = self.ui.get_widget("/MenuBar/EditMenu/LocalMenu")
        debug.log("localmenu=%s" % self.local_menu)


    def raw_convert(self, w):
        debug.log("")
        icon = self.common.icons_pixbuf["gupicasa"]
        dlg = raw.Converter(self.common.topwin, icon=icon)
        response = dlg.run()
        debug.log("response=%d" % response)
        dlg.destroy()


    def set_persist_commands_sensitivity(self):
        ed_img = self.get_edit_image()
        if ed_img is not None:
            debug.log("i=%d, dirty=%d" % (ed_img.i, ed_img.dirty))
            self.button_save.set_sensitive(ed_img.dirty)
            self.button_revert.set_sensitive(ed_img.dirty and ed_img.saved)
            self.button_remove.set_sensitive(ed_img.dirty or  ed_img.saved)


    def get_pixbuf(self, ed_photo):
        debug.log("i=%d" % ed_photo.i)
        wh = (0,0)
        rot_quad = ed_photo.get_rotation(self.detailed_mode_original)
        debug.log("rot_quad=%d" % rot_quad)
        if self.scale2fit:
            p = self.detailed_image.get_parent()
            debug.log("fit_image: parent=%s" % p)
            win = p.get_window()
            if win is not None:
                wh = self.scale_to_fit(ed_photo, p.get_window().get_size(),
                                       rot_quad)
        elif not self.detailed_mode_original:
            wh = ed_photo.value_edited.wh
        key = ((ed_photo.filename), wh, rot_quad)
        debug.log("key=%s" % str(key))
        pb = self.pixbufs_cache.get(key)
        debug.log("pb=%s" % pb)
        if pb is None:
            # Get original pixbuf
            orig_key = ("", (0,0), 0)
            if orig_key != key:
                pb = self.pixbufs_cache.get(orig_key)
                if pb is not None:
                    debug.log("HOW CAN THIS BE !!!!!!!!!????????????")
                    ugtk.warning(self.common.topwin, "Software error")
            if pb is None:
                try:
                    pb = gtk.gdk.pixbuf_new_from_file(ed_photo.filename)
                except:
                    pb = None
            if pb is not None:
                debug.log("Scale & rotate: wh=%s, rq=%d" % (wh, rot_quad))
                # Scale
                if wh != (0,0):
                    scaled_pb = pb.scale_simple(wh[0], wh[1],
                                                gtk.gdk.INTERP_TILES)
                    del pb
                    pb = scaled_pb
                # Rotate
                if rot_quad > 0:
                    gdk_rot = rot_quad_to_gdk_rot[rot_quad]
                    pb_rot = pb.rotate_simple(gdk_rot)
                    del pb
                    pb = pb_rot
            self.pixbufs_cache.set(key, pb)
        return pb


    def scale_to_fit(self, ed_photo, bound_wh, rot_quad):
        owh = wh = ed_photo.value_original.wh
        flip = ed_photo.get_rotation_relative(rot_quad) in (1, 3)
        if flip:
            bound_wh = (bound_wh[1], bound_wh[0])
        wh = rect.scale_to_fit(wh, bound_wh)
        debug.log("owh=%s, bwh=%s, wh=%s" % (str(owh), str(bound_wh), str(wh)))
        return wh


    def set_pixels_choices(self):
        epixels = self.epixels
        ed_img = self.get_edit_image()
        w, h = ed_img.value_original.wh
        if ed_img.value_original.rot_quad % 2 != 0:
            w, h = h, w
        debug.log("w=%d, h=%d" % (w, h))
        if self.current_wh != (w, h):
            self.current_wh = (w, h)
            self.pixels_divisors.clear()
            gcd = arith.gcd(w, h)
            divs = arith.divisors(gcd)
            for d in divs:
                wd = w/d
                hd = h/d
                self.pixels_divisors.append(["%dx%d" % (wd, hd), d])
        self.epixels.child.set_text(ed_img.wh_edited_strget())


    def detailed_draw(self):
        debug.log("i=%d" % self.current_image)
        ed_img = self.get_edit_image()
        if ed_img is not None:
            pixbuf = ed_img.pixbuf_detailed()
            debug.log("pixbuf=%s" % pixbuf)
            self.detailed_image.set_from_pixbuf(pixbuf)
            self.detailed_image.show()
            self.display_edited_values()
            self.set_persist_commands_sensitivity()


    def set_zoom_mode(self, fit_flag, dummy):
        debug.log("fit_flag=%d" % fit_flag)
        self.scale2fit = fit_flag
        self.detailed_draw()


    def set_edit_mode(self):
        debug.log("")
        active = not self.detailed_mode_original
        self.detailed_draw()
        for eaw in self.edit_action_widgets:
            eaw.set_sensitive(active)


    def display_edited_values(self):
        ed_img = self.get_edit_image()
        if ed_img is not None:
            self.etime.set_text(self.datetime2str(ed_img.get_datetime()))
            sfn = ed_img.saved_filename
            self.efilename.set_text(sfn if sfn is not None else "")
            self.epixels.child.set_text(ed_img.wh_edited_strget())
            self.epixels.handler_block(self.epixels_handler)
            self.set_pixels_choices()
            self.epixels.handler_unblock(self.epixels_handler)
            self.combox_rotation.handler_block(self.rotate_handler)
            self.combox_rotation.set_active(ed_img.get_rotation_relative())
            self.combox_rotation.handler_unblock(self.rotate_handler)
            self.etime.set_text(self.datetime2str(ed_img.get_datetime()))
            s = ed_img.caption
            if s is None:
                s = ""
            self.caption_entry.set_text(s)


    def on_focus(self, on):
        if on:
            msg = ""
            if self.dirname is None:
                msg = "Please open local directory"
            self.status_text_set(msg)
        self.local_menu.set_sensitive(on)


    def get_selection_numbers(self):
        n_selected = len(self.tbl_mosaic.selected_indices)
        n_selected_edited = 0
        n_selected_caption = 0
        n_dirty = 0
        for ei in self.tbl_mosaic.selected_indices:
            ed_img = self.images[ei]
            if ed_img.saved_filename is not None:
                n_selected_edited += 1
            if ed_img.caption is not None:
                n_selected_caption += 1
            if ed_img.dirty:
                n_dirty += 1
        return (n_selected, n_selected_edited, n_selected_caption, n_dirty)


    def for_selected(self, func, data): # to be obsolete
        for ei in self.tbl_mosaic.selected_indices:
            ed_img = self.images[ei]
            func(ed_img, data)


    def upload(self, upload_one, n_uploaded_target):
        debug.log("")
        uploader = Uploader(self, upload_one, n_uploaded_target)
        monitor = ugtk.Monitor(title="Uploading to Picasa album",
                               parent=self.common.topwin,
                               step_cb=uploader.step)
        done = monitor.run_monitor()
        monitor.destroy()


    def scr_dirname(self):
        return self.common.preferences.scratch_dir


    #############
    # Callbacks #
    #############


    def set_edit(self, w):
        active = w.get_active()
        debug.log("w=%s, active=%d" % (w, active))
        self.detailed_mode_original = not active
        self.set_edit_mode()


    def select_date_time(self, w, dummy):
        debug.log("")
        ed_img = self.get_edit_image()
        if ed_img is not None:
            dt = ed_img.get_datetime()
            dlg = cal.SelectDateTimeDialog(utc=True, topwin=self.common.topwin,
                                           value=dt, common=self.common)
            # Set icon
            dlg.set_icon(self.common.icons_pixbuf["gupicasa"]) #
            response = dlg.run()
            if response == gtk.RESPONSE_OK:
                dt = dlg.get_datetime()
                ed_img.set_datetime(dt)
                self.etime.set_text(self.datetime2str(dt))
                self.set_persist_commands_sensitivity()
            dlg.destroy()


    def pick_detailed(self, w, i):
        debug.log("w=%s, i=%d  active=%d, mode=%d" %
                  (w, i, w.get_active(), w.get_mode()))
        if w.get_active():
            self.current_image = i
            ed_img = self.get_edit_image()
            self.filename.set_text(ed_img.filename)
            self.pixels.set_text(ed_img.wh_original_strget())
            s = self.datetime2str(ed_img.value_original.datetime)
            self.time.set_text(s)
            self.pixels.set_tooltip_text(ed_img.orientation_text())
            self.display_edited_values()
            self.detailed_draw()


    def empty_detailed(self):
        debug.log("")
        map(lambda w: w.set_text(""), 
            [self.filename, self.pixels, self.time, self.caption_entry,
             self.efilename, self.epixels.child, self.etime])
        self.detailed_image.clear()


    def rotate(self, w):
        n = w.get_active()
        ed_img = self.get_edit_image()
        if ed_img is not None:
            debug.log("active=%d, i=%d" % (n, ed_img.i))
            ed_img.set_rotation_relative(n)
            self.set_pixels_choices()
            self.detailed_draw()


    def scale(self, w):
        n = w.get_active()
        ed_img = self.get_edit_image()
        if ed_img is not None and n >= 0:
            d = self.pixels_divisors[n][1]
            debug.log("active=%d, d=%d" % (n, d))
            ed_img.wh_edited_scale(d)
            self.detailed_draw()


    def arbitrary_scale(self, epixels_entry, dummy=None):
        ed_img = self.get_edit_image()
        if ed_img is not None:
            s = epixels_entry.get_text()
            wxh = string.strip(s)
            match = self.__class__.pattern_wwwxhhh.match(wxh)
            debug.log("%s, wxh=%s, match=%s" %
                             (s, wxh, match))
            if match:
                w = int(match.group(1))
                h = int(match.group(2))
                if not ed_img.wh_edited_check_set(w, h):
                    wxh = ed_img.wh_edited_strget()
            else:
                wxh = ed_img.wh_edited_strget()
            if s != wxh:
                epixels_entry.set_text(wxh)
            self.detailed_draw()


    def save(self, w):
        debug.log("")
        ed_img = self.get_edit_image()
        if ed_img is not None:
            pfn = ed_img.save(self.scr_dirname())
            self.efilename.set_text(pfn)
            self.set_persist_commands_sensitivity()


    def revert_to_saved(self, w):
        debug.log("")
        ed_img = self.get_edit_image()
        if ed_img is not None:
            ed_img.revert_to_saved()
            self.detailed_draw()
            self.set_persist_commands_sensitivity()


    def revert_to_original(self, w):
        debug.log("")
        ed_img = self.get_edit_image()
        if ed_img is not None:
            ed_img.revert_to_original()
            self.detailed_draw()
            self.set_persist_commands_sensitivity()


    def update_caption(self, e, dummy=None):
        debug.log("")
        ed_img = self.get_edit_image()
        if ed_img is not None:
            s = e.get_text().strip()
            if s == "":
                s = None
            ed_img.caption = s


    def select_all(self, w, dummy=None):
        debug.log("")
        map(lambda cb: cb.set_active(True), self.tbl_mosaic.cbs)


    def unselect_all(self, w, dummy=None):
        debug.log("")
        map(lambda cb: cb.set_active(False), self.tbl_mosaic.cbs)


    def select_edited_unsaved(self, w, dummy=None):
        debug.log("")
        cbs = self.tbl_mosaic.cbs
        n = 0
        for i in range(len(self.images)):
            ed_img = self.images[i]
            if ed_img.dirty:
                cbs[i].set_active(True)
                n += 1
        self.status_text_set("%d unsaved images selected" % n)



    def select_timeless(self, w, dummy=None):
        debug.log("")
        cbs = self.tbl_mosaic.cbs
        n = 0
        for i in range(len(self.images)):
            ed_img = self.images[i]
            if ed_img.value_original.datetime is None:
                cbs[i].set_active(True)
                n += 1
        self.status_text_set("%d timeless images selected" % n)


    def select_same_dimensions(self, w, dummy=None):
        debug.log("")
        n = 0
        ci = self.current_image
        if ci >= 0:
            ed_img = self.images[self.current_image]
            curr_img_dims = ed_img.value_original.wh
            for i in range(len(self.images)):
                ed_img = self.images[i]
                if (i != ci) and (ed_img.value_original.wh == curr_img_dims):
                    cbs[i].set_active(True)
                    n += 1
        self.status_text_set("%d images with same dimensions selected" % n)


    def selected_set_same_time(self, w, dummy=None):
        debug.log("")
        n = 0
        curr_img_dt = None
        ed_img = self.get_edit_image()
        if ed_img is not None:
            curr_img_dt = ed_img.value_edited.datetime
        if curr_img_dt is not None:
            for i in self.tbl_mosaic.selected_indices:
                if i != self.current_image:
                    self.images[i].set_datetime(curr_img_dt)
                    n += 1
        self.status_text_set("%d selected images set timedate" % n)


    def selected_shift_same_time(self, w, dummy=None):
        debug.log("")
        n = 0
        curr_img_dt = None
        ed_img = self.get_edit_image()
        shift_dt = None
        if ed_img is not None:
            odt = ed_img.value_original.datetime
            edt = ed_img.value_edited.datetime
            if not None in (odt, edt):
                shift_dt = edt - odt # datetime.timedelta
        if shift_dt is not None:
            for i in self.tbl_mosaic.selected_indices:
                ed_img = self.images[i]
                odt = ed_img.value_original.datetime
                if (i != self.current_image) and (odt is not None):
                    self.images[i].set_datetime(odt + shift_dt)
                    n += 1
        self.status_text_set("%d selected images shifted timedate" % n)


    def selected_same_scale(self, w, dummy=None):
        debug.log("")
        n = 0
        ed_img = self.get_edit_image()
        if ed_img is None:
            self.status_text_set("No current image")
        else:
            owh = ed_img.value_original.wh
            ewh = ed_img.value_edited.wh
            if owh == ewh:
                self.status_text_set("No scale change")
            else:
                for i in self.tbl_mosaic.selected_indices:
                    ed_img = self.images[i]
                    wh = ed_img.value_original.wh
                    if (i != self.current_image) and (wh == owh):
                        ed_img.set_wh(ewh)
                        n += 1
        self.status_text_set("%d selected images scaled" % n)



    def selected_save(self, w, dummy=None):
        debug.log("")
        n = 0
        for i in self.tbl_mosaic.selected_indices:
            ed_img = self.images[i]
            if ed_img.dirty:
                ed_img.save(self.scr_dirname())
                n += 1
        self.status_text_set("%d images saved" % n)


    def selected_remove(self, w, dummy=None):
        debug.log("")
        n = 0
        for i in self.tbl_mosaic.selected_indices:
            ed_img = self.images[i]
            if ed_img.saved:
                ed_img.revert_to_original()
                n += 1
        self.status_text_set("%d images reverted to original" % n)


    def prefs_observer(self, prefs):
        debug.log("")
        self.tbl_mosaic.n_columns = prefs.mosaic_zcolumns()
        if ((self.last_scr_clear is not None) and
            (prefs.scratch_dir != self.last_scr_clear)):
            usys.dir_remove(self.scr_dirname())
        mm = tuple(prefs.thumb_size_mm)
        if mm != (self.mapper.target_min, self.mapper.target_max):
            debug.log("Renew mapper")
            self.mapper = rect.Mapper(mm[0], mm[1])

    def clear_scratch_dir(self):
        debug.log("")
        if self.common.preferences.auto_clear:
            self.last_scr_clear = sdn = self.scr_dirname()
            self.status_text_set("Auto clear directory: %s" % sdn)
            debug.log("sdn=%s" % sdn)
            err = usys.dir_clear(sdn)
            if err is not None:
                self.common.warning("""
Failed to clear and make fresh
  %s
directory.
%s
"""[1:-1] %
                                    (sdn, err))


    #############
    # Utilities #
    #############

    def status_text_set(self, msg):
        self.common.statusbar.set_text(msg)



    def datetime2str(self, dt):
        s = "Unknown"
        if dt is not None:
            s = dt.strftime("%Y/%m=%b/%d %H:%M:%S")
        return s


    def get_edit_image(self):
        ed_img = None
        if self.current_image >= 0:
            ed_img = self.images[self.current_image]
        return ed_img


if __name__ == "__main__":
    import sys

    def create_menubar(topwin, ui):
        debug.log("")
        entries = ( ( "FileMenu", None, "_File" ),
                    ( "EditMenu", None, "_Edit" ), )
        ui_info = \
'''<ui>
  <menubar name='MenuBar'>
    <menu action='FileMenu'>
    </menu>
    <menu action='EditMenu'>
    </menu>
  </menubar>
</ui>'''
        actions = gtk.ActionGroup("Actions")
        actions.add_actions(entries)
        ui.insert_action_group(actions, 0)
        topwin.add_accel_group(ui.get_accel_group())
        mergeid = ui.add_ui_from_string(ui_info)
        mb = ui.get_widget("/MenuBar")
        mb.show_all()
        return mb

    def quit(w, *args):
        debug.log("w=%s, args=%s" % (w, str(args)))
        gtk.main_quit()
        debug.log("quit end")


    debug.log("making topwin")
    ui = gtk.UIManager()
    topwin = gtk.Window()
    topwin.set_size_request(720, 400)
    topwin.set_title("ImgDirEditor")
    # topwin.connect('destroy', lambda *w: gtk.main_quit())
    # topwin.connect('delete-event', gtk.main_quit)
    topwin.connect('destroy', quit, "des")
    topwin.connect('delete-event', quit, "del")
    vbox = gtk.VBox()
    vbox.pack_start(create_menubar(topwin, ui), False, False, 1)
    cmdbox = gtk.HBox()
    vbox.pack_start(cmdbox, False, False, 1)
    ide  = ImgDirEditor(ui, cmdbox=cmdbox, dirname=sys.argv[1])
    vbox.pack_start(ide.mainwin, expand=True, fill=True)
    topwin.add(vbox)
    topwin.show_all()
    debug.log("Calling gtk.main()")
    gtk.main()
    debug.log("Exiting")
    sys.exit(0)
